/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nutch.protocol.httpclient;

// JDK imports
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

// Commons Codec imports
import org.apache.commons.codec.binary.Base64;

// Commons Logging imports
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// Hadoop imports
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configurable;


/**
 * Implementation of RFC 2617 Basic Authentication.  Usernames and passwords are stored 
 * in standard Nutch configuration files using the following properties:
 *      http.auth.basic.<realm>.user
 *      http.auth.basic.<realm>.pass
 *
 * @author    Matt Tencati
 */
public class HttpBasicAuthentication implements HttpAuthentication, Configurable {

    public static final Logger LOG = LoggerFactory.getLogger(HttpBasicAuthentication.class);

    private static Pattern basic = Pattern.compile("[bB][aA][sS][iI][cC] [rR][eE][aA][lL][mM]=\"(\\w*)\"");
	
    private static Map<String, HttpBasicAuthentication> authMap = new TreeMap<String, HttpBasicAuthentication>();
   
    private Configuration conf = null; 
    private String challenge = null;
    private ArrayList<String> credentials = null;
    private String realm = null;


    /**
     *  Construct an HttpBasicAuthentication for the given challenge
     *  parameters. The challenge parameters are returned by the web
     *  server using a WWW-Authenticate header. This will typically be
     *  represented by single line of the form <code>WWW-Authenticate: Basic realm="myrealm"</code>
     *
     * @param  challenge  WWW-Authenticate header from web server
     */
    protected HttpBasicAuthentication(String challenge, Configuration conf) throws HttpAuthenticationException {
        
        setConf(conf);
        this.challenge = challenge;
        credentials = new ArrayList<String>();
        
        String username = this.conf.get("http.auth.basic." + challenge + ".user");
        String password = this.conf.get("http.auth.basic." + challenge + ".password");
        
        if (LOG.isTraceEnabled()) {
          LOG.trace("BasicAuthentication challenge is " + challenge);
          LOG.trace("BasicAuthentication username=" + username);
          LOG.trace("BasicAuthentication password=" + password);
        }
 
        if (username == null) {
        	throw new HttpAuthenticationException("Username for " + challenge + " is null");
        }

        if (password == null) {
        	throw new HttpAuthenticationException("Password for " + challenge + " is null");
        }
        
        byte[] credBytes = (username + ":" + password).getBytes();
        credentials.add("Authorization: Basic " + new String(Base64.encodeBase64(credBytes)));
        if (LOG.isTraceEnabled()) {
          LOG.trace("Basic credentials: " + credentials);
        }
    }


    /* ---------------------------------- *
     * <implementation:Configurable> *
     * ---------------------------------- */

    public void setConf(Configuration conf) {
      this.conf = conf;
      //if (conf.getBoolean("http.auth.verbose", false)) {
      //  LOG.setLevel(Level.FINE);
      //} else {
      //  LOG.setLevel(Level.WARNING);
      //}
    }

    public Configuration getConf() {
      return this.conf;
    }

    /* ---------------------------------- *
     * <implementation:Configurable> *
     * ---------------------------------- */


    /**
     *  Gets the Basic credentials generated by this
     *  HttpBasicAuthentication object
     *
     * @return    Credentials in the form of <code>Authorization: Basic &lt;Base64 encoded userid:password&gt;
     *
     */
    public List<String> getCredentials() {
        return credentials;
    }


   /**
    * Gets the realm attribute of the HttpBasicAuthentication object.
    * This should have been supplied to the {@link #getAuthentication(String, Configuration)}
    * static method
    *
    * @return    The realm
    */
    public String getRealm() {
        return realm;
    }

    /**
     * This method is responsible for providing Basic authentication information.  The
     * method caches authentication information for each realm so that the required
     * authentication information does not need to be regenerated for every request.
     *  
     * @param challenge The challenge string provided by the webserver.  This is the
     * text which follows the WWW-Authenticate header, including the Basic tag.
     * @return An HttpBasicAuthentication object or null 
     * if unable to generate appropriate credentials.
     */
    public static HttpBasicAuthentication getAuthentication(String challenge, Configuration conf) {
        if (challenge == null) return null;
        Matcher basicMatcher = basic.matcher(challenge);
        if (basicMatcher.matches()) {
        	String realm = basicMatcher.group(1);
	        Object auth = authMap.get(realm);
	        if (auth == null) {
	            HttpBasicAuthentication newAuth = null;
	            try {
	            	newAuth = new HttpBasicAuthentication(realm, conf);
	            } catch (HttpAuthenticationException hae) { 
                        if (LOG.isTraceEnabled()) {
	            	  LOG.trace("HttpBasicAuthentication failed for " + challenge);
                        }
	            }
	            authMap.put(realm, newAuth);
	            return newAuth;
	        } else {
	            return (HttpBasicAuthentication) auth;
	        }
        }
        return null;
    }
    
	/**
	 * Provides a pattern which can be used by an outside resource to determine if 
	 * this class can provide credentials based on simple header information.  It does
	 * not calculate any information regarding realms or challenges.
	 * 
	 * @return Returns a Pattern which will match a Basic WWW-Authenticate header.
	 */
	public static final Pattern getBasicPattern() {
		return basic;
	}
}

